/*   The MIT License
 *   
 *   Carina Engine
 *   Copyright (c) 2009 2010 2011 2012 Zdravko Velinov
 *   
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *   THE SOFTWARE.
 */

#include "carina/material-manager.hh"
#include "carina/resources.hh"
#include "carina/shader-program.hh"
#include "carina/common/logging.hh"

#include <fstream>

namespace Carina
{
MaterialManager::MaterialManager(ResourceManager& res_mgr)
    :   m_Renderer(res_mgr.getRenderer()),
        m_ResMgr(res_mgr),
        m_IncludesManager(make_aligned_shared<IncludesManager>(res_mgr))
{
    m_Renderer->registerIncludeLoader(m_IncludesManager);
}

MaterialManager::~MaterialManager()
{
}

MaterialManager::ShaderProgramSetPtr& MaterialManager::getShaderProgramSet(const string& name)
{
    ShaderProgramSetPtr& spset = m_ShaderProgramSetCache[name];
    if(!spset.get())
    {
        string spfile = m_ResMgr.getResource(name + ".csp");
        try
        {
            spset = make_aligned_unique<ShaderProgramSet>(m_ResMgr, spfile);
        }
        catch(const Exception& e)
        {
            throw Exception(string(e.what()) + ": " + spfile);
        }
    }
    return spset;
}

void MaterialManager::getMaterial(Material& material, const string& name)
{
    CE_ASSERT(m_Renderer.get(), "No rendering back-end attached to this material manager.");
    ShaderProgramSetPtr& spset = getShaderProgramSet(name);
    
    spset->getMaterial(material);
}

ShaderProgramPtr MaterialManager::getShaderProgram(const string& name)
{
    CE_ASSERT(m_Renderer.get(), "No rendering back-end attached to this material manager.");
    ShaderProgramSetPtr& spset = getShaderProgramSet(name);
    
    return spset->getShaderProgram();
}

Material::Material()
    :   m_UVal(0) {}

Material::Material(const ShaderProgramPtr& ptr)
    :   m_UVal(0),
        m_Shader(ptr) {}

Material::Material(const Material& mat)
    :   m_ProfileMap(mat.m_ProfileMap),
		m_UVal(mat.m_UVal ? &(*m_ProfileMap.find(mat.m_UVal->first)) : nullptr),
        m_Shader(mat.m_Shader) {}

Material& Material::operator=(const Material& mat)
{
    m_ProfileMap = mat.m_ProfileMap;
    m_UVal = 0;
    m_Shader = mat.m_Shader;
    return *this;
}

ShaderProgramPtr Material::getShaderProgram() { return m_Shader; }

void Material::addVariables(const MaterialProfile& map)
{
    for(ProfileMap::iterator i = m_ProfileMap.begin(); i != m_ProfileMap.end(); ++i)
    {
        i->second.variables.insert(map.variables.begin(), map.variables.end());
        i->second.textures.insert(map.textures.begin(), map.textures.end());
    }
}

void Material::addProfile(const string& profile, const MaterialProfile& map)
{
    m_ProfileMap[profile] = map;
}

void Material::useProfile(const string& profile)
{
    ProfileMap::iterator i = m_ProfileMap.find(profile);
    if(i == m_ProfileMap.end())
        THROW_EXCEPTION("unknown profile: " + profile);
    m_UVal = &(*i);
    m_Shader->useTechnique(profile);
}

void Material::setup(const Scene& scene, const Matrix4& model_mat)
{
    if(!m_UVal)
        THROW_EXCEPTION("No profile has been specified");
    RendererPtr renderer = scene.getRenderer();
    renderer->useShaderProgram(m_Shader);
    for(UniformMap::iterator i = m_UVal->second.variables.begin();
        i != m_UVal->second.variables.end(); ++i)
        i->second.setup(*m_Shader->getVariable(i->first));
    for(TextureMap::iterator i = m_UVal->second.textures.begin();
        i != m_UVal->second.textures.end(); ++i)
        i->second.setup(*m_Shader->getTexture(i->first));

    m_Shader->getVariable("sys_TransformMatrix")->set(scene.getCamera()->getTransformMatrix());
    m_Shader->getVariable("sys_ModelMatrix")->set(model_mat);
}

void Material::set(const string& name, const TexturePtr& t)
{
    if(!m_UVal)
        THROW_EXCEPTION("No profile has been specified");
    TextureMap::iterator i = m_UVal->second.textures.find(name);
    if(i == m_UVal->second.textures.end())
        THROW_EXCEPTION("unknown texture: " + name);
    i->second = t;
}

IncludesManager::IncludesManager(ResourceManager& res_mgr)
    :   m_ResMgr(res_mgr)
{
}

IncludesManager::~IncludesManager()
{
#ifndef NDEBUG
    for(auto i = m_IncludeCache.begin(); i != m_IncludeCache.end(); ++i)
	{
		// It is entirely possible something to throw exception and prevent the creation of this object.
        if(i->second && i->second->m_References)
            Log::stream(CE_LOG_WARNING) << "The following shader include file is still in use when deinitializing IncludesManager: " << i->first << std::endl;
	}
#endif
}

bool IncludesManager::loadInclude(const string& filename, const char** contents, size_t& contents_size)
{
    FileContentsPtr& fc = m_IncludeCache[filename];
    if(!fc)
    {
        string res = m_ResMgr.getResource(filename);
        std::ifstream fs(res.c_str(), std::ios::in|std::ios::binary);
        
        CE_ASSERT(fs.is_open(), "The application has failed to load the following include file: " + filename);
		if(!fs.is_open())
			return false;

        size_t len = static_cast<size_t>(fs.tellg());
        fs.seekg(0, std::ios::end);
        len = static_cast<size_t>(fs.tellg()) - len;
        fc = make_aligned_unique<FileContents>(len);
        fs.seekg(0, std::ios::beg);
        fs.read(fc->m_Contents.get(), fc->m_Contents.size());
    }
    *contents = fc->m_Contents.get();
    contents_size = fc->m_Contents.size();
    ++fc->m_References;
	return true;
}
    
void IncludesManager::freeInclude(const char* contents)
{
#ifndef NDEBUG
    for(auto i = m_IncludeCache.begin(); i != m_IncludeCache.end(); ++i)
        if(i->second->m_Contents.get() == contents)
            --i->second->m_References;
#endif
}

FileContents::FileContents(size_t size)
    :   m_Contents(size),
        m_References(0)
{
}
}
